package org.evolAI.organism.domain;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.xml.bind.annotation.XmlRootElement;

import org.apache.log4j.Logger;
import org.evolAI.organism.Constants;
import org.evolAI.organism.code.Executor;
import org.evolAI.organism.domain.Step.Condition;
import org.evolAI.organism.domain.Step.DataTypes;
import org.evolAI.organism.domain.Step.StepTypes;
import org.evolAI.organism.util.RandomNum;
import org.hibernate.annotations.GenericGenerator;

@Entity
@Table(name="Organism")
@XmlRootElement
public class Organism {

	private static final Logger logger = Logger.getLogger(Organism.class);
	
	private static final int ChangeMutationWeight = 10;
    private static final int InsertionMutationWeight = 10;
    private static final int DeletionMutationWeight = 5;
    private static final int DuplicationMutationWeight = 1;
    private static final int TotalMutationWeight = ChangeMutationWeight + InsertionMutationWeight + DeletionMutationWeight + DuplicationMutationWeight;
    private static final int MutateStepWeight = 10;
    private static final int MutateCodeWeight = 1;

	private UUID id;
    private Organism parent;
	private int generation;
	private List<Code> codes = new ArrayList<Code>();
	private LongTermMemory memoryLong;
    //private Stack _memoryShort;
    private final List<String> input;
	private final List<String> output;
    private final StringBuilder out = new StringBuilder();
    private int lastStepCount;
    private boolean selected;
    private boolean deleted = false;
    private int age = 0;
    private long liveTime = 0;
    private int degradeCounter = Constants.DegradeTime;
    private volatile boolean stop = false;

    public Organism(List<Code> codes, int generation) {
        this.id = UUID.randomUUID();
		this.generation = generation;
		this.codes = codes;
		this.memoryLong = new LongTermMemory();
        //_memoryShort = new Stack();
        this.input = new ArrayList<String>();
        this.output = new ArrayList<String>();
	}

    protected Organism(Organism organism) {
        this.id = UUID.randomUUID();
        this.parent = organism.parent;
        this.generation = organism.generation;
        for (Code code: organism.codes) {
        	codes.add(code.copy());
        }
		this.memoryLong = organism.memoryLong.copy();
        //_memoryShort = new Stack();
        this.input = new ArrayList<String>();
        this.output = new ArrayList<String>();
    }

    public Organism copy() {
        return new Organism(this);
    }

    @OneToMany(cascade=CascadeType.ALL)
    public List<Code> getCodes()  {
    	return codes;
    }
	
	public void setCodes(List<Code> codes) {
		this.codes = codes;
	}

	@OneToOne(cascade=CascadeType.ALL)
	@JoinColumn(name="memories_id")
	public LongTermMemory getMemories() {
    	return memoryLong;
    }

	public void setMemories(LongTermMemory memories) {
		
	}
	
    @Id
    @GeneratedValue(generator="system-uuid")
    @GenericGenerator(name="system-uuid", strategy = "uuid2")
    @Column(name = "organism_id", unique = true)
    public UUID getId() {
    	return id;
    }
    
    public void setId(UUID id) {
    	this.id = id;
    }

    @Transient
    public int getCodeCount() {
    	return codes.size();
    }

    @Transient
    public int getStepCount() {
    	int count = 0;
        for (Code code: codes) {
        	count += code.size();
        }
        return count;
    }

	@ManyToOne
	@JoinColumn(name="parent_id")
    public Organism getParent() {
        return parent;
    }
	
	public void setParent(Organism parent) {
		this.parent = parent;
	}

    @Transient
    public int getMemoryCount() {
    	return memoryLong.getMemoryCount();
    }

    @Transient
    public int getMemoryGroupCount() {
    	return memoryLong.size();
    }

    @Column(name = "generation")
    public int getGeneration() {
    	return generation; 
    }

    public void setGeneration(int generation) {
    	this.generation = generation;
    }
    
    @Transient
    public long getLiveTime() {
    	return liveTime;
    }

    @Transient
    public boolean isSelected() {
    	return selected;
    }
    
    public void setSelected(boolean selected) {
    	this.selected = selected;
    }
    
    @Transient
    public boolean isDeleted() {
    	return deleted;
    }
    
    public void setDeleted(boolean deleted) {
    	this.deleted = deleted;
    }
    
    @Transient
    public String getResponses() {
        StringBuilder response = new StringBuilder();
        for (int i = output.size() - 1; i >= 0; i--) {
            if (output.get(i).length() > 0) {
                if (output.get(i).length() > 128) {
                    response.append(output.get(i).substring(0, 128)).append("\n");
                }
                else {
                    response.append(output.get(i)).append("\n");
                }
            }
        }
        return response.toString();
    }

    @Transient
    public String getInputs() {
            StringBuilder inputs = new StringBuilder();
            for (int i = input.size() - 1; i >= 0; i--)
            {
                if (input.get(i).length() > 0)
                    inputs.append(input.get(i)).append("\n");
            }
            return inputs.toString();
    }

    @Transient
    public boolean isStop() {
    	return stop;
    }
 
    @Transient
    private Code getRandomCode() {
        if (codes.size() > 0) {
            int weight = 0;
            int rndStep = RandomNum.getUniform(getStepCount());
            for (int i = 0; i < codes.size(); i++) {
                weight += codes.get(i).size();
                if (rndStep < weight)
                    return codes.get(i);
            }
        }
        return null;
    }

    //make random changes to codes
	public void mutate() {
        parent = this;
        id = UUID.randomUUID();
        generation++;

        int numMutations = Math.min(RandomNum.getNormal(1, 10), getStepCount());
        //int numMutations = Math.Min(RandomNum.GetUniform(1, 3), this.StepCount);

        for (int i = 0; i < numMutations; i++) {
            createMutation();
        }
	}

    private void createMutation() {
        int nextMutation = RandomNum.getUniform(TotalMutationWeight);
        Code rndCode = getRandomCode();
        if (rndCode != null) {
            if (nextMutation < ChangeMutationWeight) {
                //change
                if (rndCode.size() > 0) {
                    int stepIndex = RandomNum.getUniform(rndCode.size());
                    rndCode.get(stepIndex).changeRandom(this);
                }
            }
            else if (nextMutation < ChangeMutationWeight + InsertionMutationWeight) {
                //insertion
                int codeOrStep = RandomNum.getUniform(MutateStepWeight + MutateCodeWeight);
                if (codeOrStep < MutateStepWeight) {
                    if (rndCode.size() > 0) {
                        int stepIndex = RandomNum.getUniform(rndCode.size());
                        rndCode.add(stepIndex, Step.CreateRandom(this));
                    }
                }
                else {
                    Code newCode = new Code();
                    Step newStep = Step.CreateRandom(this);
                    newCode.add(newStep);
                    codes.add(RandomNum.getUniform(codes.size()), newCode);
                }
            }
            else if (nextMutation < ChangeMutationWeight + InsertionMutationWeight + DeletionMutationWeight) {
                //deletion
                int codeOrStep = RandomNum.getUniform(MutateStepWeight + MutateCodeWeight);
                if (codeOrStep < MutateStepWeight) {
                    if (rndCode.size() > 0) {
                        int stepIndex = RandomNum.getUniform(rndCode.size());
                        rndCode.remove(stepIndex);
                    }
                }
                else {
                    codes.remove(rndCode);
                }
            }
            else {
                //duplication
                int codeOrStep = RandomNum.getUniform(MutateStepWeight + MutateCodeWeight);
                if (codeOrStep < MutateStepWeight) {
                    if (rndCode.size() > 0) {
                        int stepIndex = RandomNum.getUniform(rndCode.size());
                        rndCode.add(stepIndex, rndCode.get(stepIndex).copy());
                    }
                }
                else {
                    codes.add(RandomNum.getUniform(codes.size()), rndCode.copy());
                }
            }
        }
    }


    //combine two organisms
    public Organism mate(Organism partner) {
        //todo:  determine viability of partner and mate if possible
        for (int i = 0; i < codes.size(); i++) {

        }
        return new Organism(new ArrayList<Code>(), generation++);
    }

	//rate organism fitness
	public int fitness() {
		//todo:  rate complexity of responses
		return 1;
	}

	//perfom organism functions
	public void live(int maxDepth, int maxSteps) {
		if (input.isEmpty())
			return;
        age++;
        degradeCounter--;
        if (degradeCounter < 0)
        {
            degradeMemory();
            degradeCounter = Constants.DegradeTime;
        }

        Executor aExec = new Executor(codes, this, maxDepth, maxSteps);
        String in = input.get(input.size() - 1);
        for (int i = in.length() - 1; i >= 0; i--) {
            aExec.getStrings().push(Character.toString(in.charAt(i)));
        }
        try {
        	aExec.execute(0, 0);
        }
        catch (Exception ex) {
        	logger.fatal("Organism error", ex);
        }
        lastStepCount = aExec.getStepCount();
        output.add(out.toString());
    }

	public void sendMessage(String message) {
        input.add(message);
        output.add("");
        out.replace(0, out.length(), "");
	}

    public void requestStop() {
        stop = true;
    }

    public void resetStop() {
        stop = false;
    }

	public void save(String fileName) {
		try {
			OutputStream file = new FileOutputStream(fileName);
			OutputStream buffer = new BufferedOutputStream(file);
			ObjectOutput output = new ObjectOutputStream(new GZIPOutputStream(buffer));
			try {
				output.writeObject(this);
			}
			finally {
				output.close();
			}
		}
		catch(IOException ex) {
			
		}
	}

	public static Organism load(String fileName) {
		try {
			InputStream file = new FileInputStream(fileName);
			InputStream buffer = new BufferedInputStream(file);
			ObjectInput input = new ObjectInputStream(new GZIPInputStream(buffer));
			try {
				Organism org = (Organism)input.readObject();
				return org;
			}
			finally {
				input.close();
			}
		}
		catch(ClassNotFoundException ex) {
			
		}
		catch(IOException ex) {
			
		}
		return null;
	}

	public static Organism defaultOrganism() {
        List<Code> Codes = new ArrayList<Code>();
        Code newCode;
        Step step;

        // 0 - start
        newCode = new Code();

        step = new Step(StepTypes.Branch);
        step.setData(7);
        newCode.add(step);

        step = new Step(StepTypes.Load);
        step.setData("");
        newCode.add(step);

        step = new Step(StepTypes.Branch);
        step.setData(8);
        newCode.add(step);

        Codes.add(newCode);

        // 1 - find word from characters on the string stack
        newCode = new Code();

        step = new Step(StepTypes.Load);
        step.setData("");
        newCode.add(step);

        step = new Step(StepTypes.StoreLocal);
        step.setData(0);
        step.setDataType(DataTypes.String);
        newCode.add(step);

        step = new Step(StepTypes.Marker);
        newCode.add(step);

        //check if current character is a word terminator
        step = new Step(StepTypes.Branch);
        step.setData(2);
        newCode.add(step);

        step = new Step(StepTypes.Compare);
        step.setData(1);
        newCode.add(step);

        step = new Step(StepTypes.JumpNextMarker);
        step.setCondition(Condition.Equal);
        newCode.add(step);

        // add current character to word
        step = new Step(StepTypes.LoadLocal);
        step.setData(0);
        step.setDataType(DataTypes.String);
        newCode.add(step);

        step = new Step(StepTypes.Add);
        step.setDataType(DataTypes.String);
        newCode.add(step);

        step = new Step(StepTypes.StoreLocal);
        step.setData(0);
        step.setDataType(DataTypes.String);
        newCode.add(step);

        step = new Step(StepTypes.JumpPrevMarker);
        newCode.add(step);

        step = new Step(StepTypes.Marker);
        newCode.add(step);

        step = new Step(StepTypes.LoadLocal);
        step.setData(0);
        step.setDataType(DataTypes.String);
        newCode.add(step);

        step = new Step(StepTypes.Dup);
        step.setDataType(DataTypes.String);
        newCode.add(step);

        step = new Step(StepTypes.Compare);
        step.setData("");
        newCode.add(step);

        step = new Step(StepTypes.Return);
        step.setCondition(Condition.NotEqual);
        newCode.add(step);

        step = new Step(StepTypes.Add);
        step.setDataType(DataTypes.String);
        newCode.add(step);

        Codes.add(newCode);


        // 2 - check for word terminator
        newCode = new Code();

        step = new Step(StepTypes.Branch);
        step.setData(3);
        newCode.add(step);

        step = new Step(StepTypes.JumpNextMarker);
        step.setCondition(Condition.Equal);
        newCode.add(step);

        step = new Step(StepTypes.Branch);
        step.setData(4);
        newCode.add(step);

        step = new Step(StepTypes.JumpNextMarker);
        step.setCondition(Condition.Equal);
        newCode.add(step);

        step = new Step(StepTypes.Branch);
        step.setData(5);
        newCode.add(step);

        step = new Step(StepTypes.JumpNextMarker);
         step.setCondition(Condition.Equal);
        newCode.add(step);

        step = new Step(StepTypes.Branch);
        step.setData(6);
        newCode.add(step);

        step = new Step(StepTypes.JumpNextMarker);
        step.setCondition(Condition.Equal);
        newCode.add(step);

        step = new Step(StepTypes.Load);
        step.setData(0);
        newCode.add(step);

        step = new Step(StepTypes.Return);
        newCode.add(step);

        step = new Step(StepTypes.Marker);
        newCode.add(step);

        step = new Step(StepTypes.Load);
        step.setData(1);
        newCode.add(step);

        Codes.add(newCode);

        // 3 - check for ' '
        newCode = new Code();

        step = new Step(StepTypes.Dup);
        step.setDataType(DataTypes.String);
        newCode.add(step);

        step = new Step(StepTypes.Compare);
        step.setData(" ");
        newCode.add(step);

        Codes.add(newCode);

        // 4 - check for '.'
        newCode = new Code();

        step = new Step(StepTypes.Dup);
        step.setDataType(DataTypes.String);
        newCode.add(step);

        step = new Step(StepTypes.Compare);
        step.setData(".");
        newCode.add(step);

        Codes.add(newCode);

        // 5 - check for ','
        newCode = new Code();

        step = new Step(StepTypes.Dup);
        step.setDataType(DataTypes.String);
        newCode.add(step);

        step = new Step(StepTypes.Compare);
        step.setData(",");
        newCode.add(step);

        Codes.add(newCode);

        // 6 - check for '?'
        newCode = new Code();

        step = new Step(StepTypes.Dup);
        step.setDataType(DataTypes.String);
        newCode.add(step);

        step = new Step(StepTypes.Compare);
        step.setData("?");
        newCode.add(step);

        Codes.add(newCode);

        // 7 - start
        newCode = new Code();

        step = new Step(StepTypes.Marker);
        newCode.add(step);

        // get a word
        step = new Step(StepTypes.Branch);
        step.setData(1);
        newCode.add(step);

        step = new Step(StepTypes.Dup);
        step.setDataType(DataTypes.String);
        newCode.add(step);

        step = new Step(StepTypes.Compare);
        step.setData("");
        newCode.add(step);

        // if word is "" then stop
        step = new Step(StepTypes.JumpNextMarker);
        step.setCondition(Condition.Equal);
        newCode.add(step);

        step = new Step(StepTypes.Branch);
        step.setData(9);
        newCode.add(step);

        step = new Step(StepTypes.JumpPrevMarker);
        newCode.add(step);

        step = new Step(StepTypes.Marker);
        newCode.add(step);

        Codes.add(newCode);

        // 8 - add strongest memory to output
        newCode = new Code();

        step = new Step(StepTypes.RecallStrongest);
        step.setDataType(DataTypes.String);
        newCode.add(step);

        step = new Step(StepTypes.AddToOutput);
        step.setDataType(DataTypes.String);
        newCode.add(step);

        Codes.add(newCode);

        // 9 - remember word
        newCode = new Code();

        step = new Step(StepTypes.Load);
        step.setData("");
        newCode.add(step);

        step = new Step(StepTypes.Remember);
        newCode.add(step);

        Codes.add(newCode);

        return new Organism(Codes, 1);
	}
	
	public void addToOutput(String word) {
        out.append(word);
    }

	@Transient
    public int getLastStepCount() {
    	return lastStepCount;
    }

	public void rememberLong(String key, String memory) {
        memoryLong.remember(key, memory);
	}

    public MemoryGroup recallLong(String Key) {
        return memoryLong.recall(Key);
    }

    public String recallRandom() {
        return memoryLong.recallRandom();
    }

    public String recallRandom(String key) {
        return memoryLong.recallRandom(key);
    }

    public String recallEarliest(String key) {
        return memoryLong.recallEarliest(key);
    }

    public String recallLatest(String key) {
        return memoryLong.recallLatest(key);
    }

    public String recallStrongest(String key) {
        return memoryLong.recallStrongest(key);
    }

    public void degradeMemory() {
        memoryLong.degrade();
    }

    public void clearMemory() {
        memoryLong = new LongTermMemory();
    }

	
}
